package com.bigknow.appstore.auth.service.impl;

import com.bigknow.appstore.auth.domain.oauth.*;
import com.bigknow.appstore.auth.infrastructure.DateUtils;
import com.bigknow.appstore.auth.service.OauthService;
import com.bigknow.appstore.web.console.app.Application;
import com.bigknow.appstore.web.console.app.ClientDetail;
import com.bigknow.fusion.common.model.client.FrontClientConfig;
import org.apache.oltu.oauth2.as.issuer.OAuthIssuer;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.utils.OAuthUtils;
import org.apache.shiro.SecurityUtils;
import org.mongodb.morphia.Datastore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Set;

/**
 * 2016/9/14
 *
 * @author zhangwei
 */
@Service("oauthService")
public class OauthServiceImpl implements OauthService {

    private static final Logger LOG = LoggerFactory.getLogger(OauthServiceImpl.class);

    @Autowired
    private OauthRepository oauthRepository;
    @Autowired
    private AuthenticationIdGenerator authenticationIdGenerator;
    @Autowired
    private OAuthIssuer oAuthIssuer;

    @Autowired
    private Datastore ds;

    @Override
    public ClientDetails loadClientDetails(String clientId) {
        LOG.debug("Load ClientDetails by clientId: {}", clientId);
        return generateClientDetail(clientId);
        //mongodb实现待扩展
//        return clientDetailWrapper(ds.find(ClientDetail.class).filter("client_id", clientId).get());
        //mysql实现
//        return oauthRepository.findClientDetails(clientId);
    }

    private ClientDetails generateClientDetail(String clientId) {
        Application app = ds.find(Application.class).filter("ssoConfig.clientId", clientId).get();
        if (app != null) {
            ClientDetails detail = new ClientDetails();
            detail.setClientId(app.getSsoConfig().getClientId());
            detail.setClientSecret(app.getSsoConfig().getClientSecret());
            detail.setGrantTypes(app.getSsoConfig().getGrantType());
            return detail;
        }
        return null;
    }


    private ClientDetails clientDetailWrapper(ClientDetail detail) {
        ClientDetails _detail = new ClientDetails();
        _detail.setClientId(detail.getClient_id());
        _detail.setDescription(detail.getDescription());
        _detail.setName(detail.getClient_name());
        _detail.setClientSecret(detail.getClient_secret());
        _detail.setClientUri(detail.getClient_uri());
        _detail.setIconUri(detail.getClient_icon_uri());
        _detail.setRedirectUri(detail.getRedirect_uri());
        _detail.setRoles(detail.getRoles());
        _detail.setScope(detail.getScope());
        _detail.setAccessTokenValidity(detail.getAccess_token_validity());
        _detail.setGrantTypes(detail.getGrant_types());
        _detail.setRefreshTokenValidity(detail.getRefresh_token_validity());
        _detail.setResourceIds(detail.getResource_ids());
        _detail.setTrusted(detail.getTrusted() == 0 ? true : false);
        _detail.setArchived(detail.getArchived() != 0 ? true : false);
        _detail.setCreateTime(DateUtils.now());
        return _detail;
    }


    @Override
    public OauthCode saveAuthorizationCode(String authCode, ClientDetails clientDetails) {
        final String username = currentUsername();
        OauthCode oauthCode = new OauthCode()
                .code(authCode).username(username)
                .clientId(clientDetails.getClientId());

        oauthRepository.saveOauthCode(oauthCode);
        LOG.debug("Save OauthCode: {}", oauthCode);
        return oauthCode;
    }

    private String currentUsername() {
        return (String) SecurityUtils.getSubject().getPrincipal();
    }

    @Override
    public String retrieveAuthCode(ClientDetails clientDetails) throws OAuthSystemException {
        final String clientId = clientDetails.getClientId();
        final String username = currentUsername();

        OauthCode oauthCode = oauthRepository.findOauthCodeByUsernameClientId(username, clientId);
        if (oauthCode != null) {
            LOG.debug("OauthCode ({}) is existed, remove it and create a new one", oauthCode);
            oauthRepository.deleteOauthCode(oauthCode);
        }
        oauthCode = createOauthCode(clientDetails);

        return oauthCode.code();
    }


    @Override
    public AccessToken retrieveAccessToken(ClientDetails clientDetails, Set<String> scopes, boolean includeRefreshToken) throws OAuthSystemException {
        String scope = OAuthUtils.encodeScopes(scopes);
        final String username = currentUsername();
        final String clientId = clientDetails.getClientId();

        final String authenticationId = authenticationIdGenerator.generate(clientId, username, scope);

        AccessToken accessToken = findAccessToken(clientId, username, authenticationId);
        if (accessToken == null) {
            accessToken = createAndSaveAccessToken(clientDetails, includeRefreshToken, username, authenticationId);
            LOG.debug("Create a new AccessToken: {}", accessToken);
        }

        return accessToken;
    }

    //Always return new AccessToken, exclude refreshToken
    @Override
    public AccessToken retrieveNewAccessToken(ClientDetails clientDetails, Set<String> scopes) throws OAuthSystemException {
        String scope = OAuthUtils.encodeScopes(scopes);
        final String username = currentUsername();
        final String clientId = clientDetails.getClientId();

        final String authenticationId = authenticationIdGenerator.generate(clientId, username, scope);

        AccessToken accessToken = findAccessToken(clientId, username, authenticationId);
        if (accessToken != null) {
            LOG.debug("Delete existed AccessToken: {}", accessToken);
            deleteAccessToken(accessToken);
        }
        accessToken = createAndSaveAccessToken(clientDetails, false, username, authenticationId);
        LOG.debug("Create a new AccessToken: {}", accessToken);

        return accessToken;
    }

    @Override
    public OauthCode loadOauthCode(String code, ClientDetails clientDetails) {
        final String clientId = clientDetails.getClientId();
        com.bigknow.appstore.web.console.app.OauthCode _code = ds.find(com.bigknow.appstore.web.console.app.OauthCode.class)
                .filter("client_id", clientDetails.getClientId()).filter("code", code).get();
        return oauthCodeWrapper(_code);
    }

    private OauthCode oauthCodeWrapper(com.bigknow.appstore.web.console.app.OauthCode _code) {
        if (_code == null) {
            return null;
        }
        OauthCode code = new OauthCode();
        code.setCode(_code.getCode());
        code.setClientId(_code.getClient_id());
        code.setUsername(_code.getUser_name());
        return code;
    }

    @Override
    public boolean removeOauthCode(String code, ClientDetails clientDetails) {
//        final OauthCode oauthCode = loadOauthCode(code, clientDetails);
//        final int rows = oauthRepository.deleteOauthCode(oauthCode);
        com.bigknow.appstore.web.console.app.OauthCode _oauthCode = ds.find(com.bigknow.appstore.web.console.app.OauthCode.class).filter("code", code).filter("client_id", clientDetails.getClientId()).get();
        if (code != null) {
            ds.delete(_oauthCode);
        }
        return true;
    }

    //Always return new AccessToken
    @Override
    public AccessToken retrieveAuthorizationCodeAccessToken(ClientDetails clientDetails, String code) throws OAuthSystemException {
        final OauthCode oauthCode = loadOauthCode(code, clientDetails);
        final String username = oauthCode.username();
        final String clientId = clientDetails.getClientId();
        final String authenticationId = authenticationIdGenerator.generate(clientId, username, null);

        AccessToken accessToken = findAccessToken(clientId, username, authenticationId);
        if (accessToken != null) {
            LOG.debug("Delete existed AccessToken: {}", accessToken);
            deleteAccessToken(accessToken);
        }
        accessToken = createAndSaveAccessToken(clientDetails, clientDetails.supportRefreshToken(), username, authenticationId);
        LOG.debug("Create a new AccessToken: {}", accessToken);
        return accessToken;
    }

    private AccessToken findAccessToken(String clientId, String username, String authenticationId) {
        com.bigknow.appstore.web.console.app.AccessToken _token = ds.find(com.bigknow.appstore.web.console.app.AccessToken.class).filter("client_id", clientId).filter("username", username).filter("authentication_id", authenticationId).get();
        return AccessTokenWrapper(_token);
    }


    private AccessToken AccessTokenWrapper(com.bigknow.appstore.web.console.app.AccessToken _token) {
        if (_token == null) {
            return null;
        }
        AccessToken token = new AccessToken();
        token.setUsername(_token.getUsername());
        token.setAuthenticationId(_token.getAuthentication_id());
        token.setClientId(_token.getClient_id());
        token.setRefreshToken(_token.getRefresh_token());
        token.setRefreshTokenExpiredSeconds(_token.getRefresh_token_expired_seconds());
        token.setTokenExpiredSeconds(_token.getToken_expired_seconds());
        token.setTokenId(_token.getToken_id());
        token.setTokenType(_token.getToken_type());
        return token;
    }


    private void deleteAccessToken(AccessToken accessToken) {
        ds.delete(ds.find(com.bigknow.appstore.web.console.app.AccessToken.class).filter("token_id", accessToken.tokenId()).get());
    }

    //grant_type=password AccessToken
    @Override
    public AccessToken retrievePasswordAccessToken(ClientDetails clientDetails, Set<String> scopes, String username) throws OAuthSystemException {
        String scope = OAuthUtils.encodeScopes(scopes);
        final String clientId = clientDetails.getClientId();

        final String authenticationId = authenticationIdGenerator.generate(clientId, username, scope);
        AccessToken accessToken = findAccessToken(clientId, username, authenticationId);

        boolean needCreate = false;
        if (accessToken == null) {
            needCreate = true;
            LOG.debug("Not found AccessToken from repository, will create a new one, client_id: {}", clientId);
        } else if (accessToken.tokenExpired()) {
            LOG.debug("Delete expired AccessToken: {} and create a new one, client_id: {}", accessToken, clientId);
            deleteAccessToken(accessToken);
            needCreate = true;
        } else {
            LOG.debug("Use existed AccessToken: {}, client_id: {}", accessToken, clientId);
        }
        if (needCreate) {
            accessToken = createAndSaveAccessToken(clientDetails, clientDetails.supportRefreshToken(), username, authenticationId);
            LOG.debug("Create a new AccessToken: {}", accessToken);
        }
        return accessToken;

    }


    //grant_type=client_credentials
    @Override
    public AccessToken retrieveClientCredentialsAccessToken(ClientDetails clientDetails, Set<String> scopes) throws OAuthSystemException {
        String scope = OAuthUtils.encodeScopes(scopes);
        final String clientId = clientDetails.getClientId();
        //username = clientId

        final String authenticationId = authenticationIdGenerator.generate(clientId, clientId, scope);
        AccessToken accessToken = findAccessToken(clientId, clientId, authenticationId);

        boolean needCreate = false;
        if (accessToken == null) {
            needCreate = true;
            LOG.debug("Not found AccessToken from repository, will create a new one, client_id: {}", clientId);
        } else if (accessToken.tokenExpired()) {
            LOG.debug("Delete expired AccessToken: {} and create a new one, client_id: {}", accessToken, clientId);
            deleteAccessToken(accessToken);
            needCreate = true;
        } else {
            LOG.debug("Use existed AccessToken: {}, client_id: {}", accessToken, clientId);
        }

        if (needCreate) {
            //Ignore refresh_token
            accessToken = createAndSaveAccessToken(clientDetails, false, clientId, authenticationId);
            LOG.debug("Create a new AccessToken: {}", accessToken);
        }

        return accessToken;

    }

    @Override
    public AccessToken loadAccessTokenByRefreshToken(String refreshToken, String clientId) {
        LOG.debug("Load ClientDetails by refreshToken: {} and clientId: {}", refreshToken, clientId);
        return oauthRepository.findAccessTokenByRefreshToken(refreshToken, clientId);
    }

    /*
    * Get AccessToken
    * Generate a new AccessToken from existed(exclude token,refresh_token)
    * Update access_token,refresh_token, expired.
    * Save and remove old
    * */
    @Override
    public AccessToken changeAccessTokenByRefreshToken(String refreshToken, String clientId) throws OAuthSystemException {
        final AccessToken oldToken = loadAccessTokenByRefreshToken(refreshToken, clientId);

        AccessToken newAccessToken = oldToken.cloneMe();
        LOG.debug("Create new AccessToken: {} from old AccessToken: {}", newAccessToken, oldToken);

        ClientDetails details = oauthRepository.findClientDetails(clientId);
        newAccessToken.updateByClientDetails(details);

        final String authId = authenticationIdGenerator.generate(clientId, oldToken.username(), null);
        newAccessToken.authenticationId(authId)
                .tokenId(oAuthIssuer.accessToken())
                .refreshToken(oAuthIssuer.refreshToken());

        deleteAccessToken(oldToken);
        LOG.debug("Delete old AccessToken: {}", oldToken);

//        oauthRepository.saveAccessToken(newAccessToken);
        saveAccessToken(newAccessToken);
        LOG.debug("Save new AccessToken: {}", newAccessToken);

        return newAccessToken;
    }

    @Override
    public boolean isExistedClientId(String clientId) {
        final ClientDetails clientDetails = loadClientDetails(clientId);
        return clientDetails != null;
    }

    private void saveAccessToken(AccessToken token) {
        com.bigknow.appstore.web.console.app.AccessToken dbToken = ds.find(com.bigknow.appstore.web.console.app.AccessToken.class).filter("client_id", token.getClientId()).get();
        if (dbToken == null) {
            com.bigknow.appstore.web.console.app.AccessToken _token = new com.bigknow.appstore.web.console.app.AccessToken();
            _token.setAuthentication_id(token.authenticationId());
            _token.setClient_id(token.clientId());
            _token.setCreate_time(token.createTime().getTime());
            _token.setRefresh_token(token.refreshToken());
            _token.setRefresh_token_expired_seconds(token.refreshTokenExpiredSeconds());
            _token.setToken_expired_seconds(token.tokenExpiredSeconds());
            _token.setToken_id(token.tokenId());
            _token.setToken_type(token.tokenType());
            _token.setUsername(token.username());
            ds.save(_token);
        }
    }

    private AccessToken createAndSaveAccessToken(ClientDetails clientDetails, boolean includeRefreshToken, String username, String authenticationId) throws OAuthSystemException {
        AccessToken accessToken = new AccessToken()
                .clientId(clientDetails.getClientId())
                .username(username)
                .tokenId(oAuthIssuer.accessToken())
                .authenticationId(authenticationId)
                .updateByClientDetails(clientDetails);
        if (includeRefreshToken) {
            accessToken.refreshToken(oAuthIssuer.refreshToken());
        }
//        this.oauthRepository.saveAccessToken(accessToken);
        saveAccessToken(accessToken);
        LOG.debug("Save AccessToken: {}", accessToken);
        return accessToken;
    }

    private OauthCode createOauthCode(ClientDetails clientDetails) throws OAuthSystemException {
        OauthCode oauthCode;
        final String authCode = oAuthIssuer.authorizationCode();

        LOG.debug("Save authorizationCode '{}' of ClientDetails '{}'", authCode, clientDetails);
        oauthCode = this.saveAuthorizationCode(authCode, clientDetails);
        return oauthCode;
    }


}
